﻿using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Timers;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using log4net;
using System;

namespace Updater.Common
{
    public class Downloader
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Downloader));

        private string m_downloadPath;
        public string DownloadPath
        {
            get { return m_downloadPath; }
        }

        private List<SiteInfo> siteInfos;
        private List<SiteInfo> sitesToUpdate;
        
        public Downloader()
        {
            siteInfos = new List<SiteInfo>();
            sitesToUpdate = new List<SiteInfo>();

            ReadUpdaterSettings();            
        }

        Timer m_Timer;

        #region Timer methods
        /// <summary>
        /// Is called to initiate the automatic download poll functionality.
        /// </summary>
        public void Start()
        {
            m_Timer = new Timer();
            //handle Elapsed event 
            m_Timer.Elapsed += new ElapsedEventHandler(OnElapsedTime);
            //set interval to 1 minute (= 60,000 milliseconds)
            m_Timer.Interval = Properties.Settings.Default.CheckEveryMilliSeconds;
            //enabling the timer
            m_Timer.Enabled = true;
            //DoDownloads();
            //DoUpdates();
        }

        /// <summary>
        /// Timer to start 
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private void OnElapsedTime(object source, ElapsedEventArgs e)
        {
            //Disabe timer until operation was finished.
            m_Timer.Enabled = false;
            DoDownloads();
            DoUpdates();
            m_Timer.Enabled = true;

        }

        /// <summary>
        ///  Is called to stop the automatic poll download functioanlity.
        /// </summary>

        public void Stop()
        {
            m_Timer.Enabled = false;
            //WriteVersionsToUpdaterSettings();
            log.Debug("Timer was stopped");
        }

        #endregion

        /// <summary>
        /// Write current versions to UpdaterSettings.xml
        /// </summary>
        private void WriteVersionsToUpdaterSettings()
        {
            try
            {
                string path = Properties.Settings.Default.UpdaterSettingPath;
                XmlDocument doc = new XmlDocument();
                using (FileStream configFile = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    doc.Load(configFile);
                }

                XmlNode downloadPathNode = doc.DocumentElement.SelectSingleNode("DownloadPath");
                m_downloadPath = downloadPathNode.Attributes["directory"].Value;

                foreach (XmlNode siteInfoNode in doc.DocumentElement.SelectSingleNode("Sites").SelectNodes("Site"))
                {
                    string id = siteInfoNode.Attributes["id"].Value;
                    var siteInfo = siteInfos.FirstOrDefault(si => si.Id == id);
                    if(siteInfo != null)
                        siteInfo.SaveCurrentVersionValue(siteInfoNode);
                }
                doc.Save(path);
            }

            catch (Exception exception)
            {
                log.Error("Saving of UpdaterSettings.xml failed", exception);
            }
        }

        /// <summary>
        /// Read settings and siteInfos from UpdaterSettings.xml
        /// </summary>
        private void ReadUpdaterSettings()
        {
            try
            {
                string path = Properties.Settings.Default.UpdaterSettingPath;
                XmlDocument doc = new XmlDocument();
                using (FileStream configFile = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    doc.Load(configFile);
                }

                XmlNode downloadPathNode = doc.DocumentElement.SelectSingleNode("DownloadPath");
                m_downloadPath = downloadPathNode.Attributes["directory"].Value;

                foreach (XmlNode siteInfoNode in doc.DocumentElement.SelectSingleNode("Sites").SelectNodes("Site"))
                {
                    siteInfos.Add(new SiteInfo(siteInfoNode));
                }
            }

            catch (Exception exception)
            {
                log.Error("Loading of UpdaterSettings.xml failed", exception);
                siteInfos.Clear();
                sitesToUpdate.Clear();
            }
        }

        /// <summary>
        /// Downloads available updates to the DownloadPath directory.
        /// </summary>
        public void DoDownloads()
        {
            string versionToDownload = "";
            ServiceReference.ServiceClient serviceClient = null;

            log.Debug("DoDownload");

            try
            {
                serviceClient = new Updater.Common.ServiceReference.ServiceClient("WSHttpBinding_IService");
                serviceClient.Open();

                foreach (SiteInfo siteInfo in siteInfos)
                {
                    versionToDownload = serviceClient.NameOfNewerVersion(siteInfo.ApplicationNameWithExtension + "." + siteInfo.VersionNumber);
                    if (!string.IsNullOrEmpty(versionToDownload) && siteInfo.AcceptsVersionType(versionToDownload))
                    {
                        if (!FileExists(versionToDownload)) // just a check for security reasions
                        {
                            log.Debug("version to download:" + versionToDownload);
                            log.Debug("Delete temporary files");
                            DeleteTemporaryFiles();
                            log.Debug("Download file");
                            if (DownloadFile(versionToDownload, serviceClient))
                            {
                                log.Debug("Rename downloaded file");
                                RenameDownloadedFile(versionToDownload);
                            }
                        }
                    }
                }
            }
            catch( Exception ex )
            {
                log.Error("Download failed", ex);
            }
            finally
            {
                serviceClient.Close();
            }
        }

        /// <summary>
        /// Installs updates
        /// </summary>
        public void DoUpdates()
        {
            log.Debug("DoUpdates");

            List<String> installationFiles = null;
            bool installSuccessfull = true;
            foreach (SiteInfo siteInfo in siteInfos)
            {
                if (siteInfo.IsTimeToInstall(DateTime.Now))
                {
                    installationFiles = GetFilesToInstall(siteInfo.ApplicationNameWithExtension, siteInfo.VersionNumber);
                    if( installationFiles.Count != 0 )
                    {
                        siteInfo.NotifyUser();
                        foreach (string installationFile in installationFiles)
                        {
                            log.Info(string.Format("Install  {0}", installationFile));
                            installSuccessfull = siteInfo.DoInstall(installationFile,m_downloadPath);
                            if (installSuccessfull)
                            {
                                log.Info(string.Format("Installation of {0} success", installationFile));
                                siteInfo.VersionNumber = VersionOfFile(installationFile);
                                MarkDownloadInSiteInfo(siteInfo);
                                // TODO: Notify users
                            }
                            else
                            {
                                log.Error(string.Format("Installation of {0} failed", installationFile));
                                break; // stop further installs
                            }
                        }
                    }                    
                }
            }
        }

        /// <summary>
        /// Removes the "TEMP_" prefix from the temporary copy of the downloaded file.
        /// </summary>
        /// <param name="filename">Name of downloaded file</param>
        private void RenameDownloadedFile(string filename)
        {
            try
            {
                File.Move(Path.Combine(m_downloadPath, "TEMP_" + filename), Path.Combine(m_downloadPath, filename));
            }
            catch( Exception ex )
            {
                log.Error("Could not find temporary copy of downloaded file", ex);
            }
        }

        /// <summary>
        /// Tries three times to download the file.
        /// </summary>
        /// <param name="filename">File to download</param>
        /// <param name="serviceClient">Service to use</param>
        /// <returns></returns>
        private bool DownloadFile(string filename, ServiceReference.ServiceClient serviceClient )
        {
            bool fileDownloaded = true;
            string filePath = Path.Combine(m_downloadPath, "TEMP_" + filename);
            Stream readStream = null;
            FileStream saveFile = null;

            // Try downloading 3 times.
            for (int i = 0; i < 3; i++)
            {
                try
                {
                    readStream = serviceClient.GetBinaryStream(filename);                    
                    saveFile = File.Open(filePath, FileMode.Create, FileAccess.Write);

                    using (BinaryReader reader = new BinaryReader(readStream))
                    {
                        int bytesSize = 0;
                        byte[] downBuffer = new byte[1024];

                        while ((bytesSize = reader.Read(downBuffer, 0, downBuffer.Length)) != 0)
                        {
                            saveFile.Write(downBuffer, 0, bytesSize);
                        }
                    }                    
                    saveFile.Close(); // necessary before call to GetFileHashCode

                    fileDownloaded = (serviceClient.GetFileHashCode(filename) == GetFileHashCode(filePath));
                    if (readStream == null)
                    {
                        fileDownloaded = false;
                        log.Error("File was not found by service during download");
                    }
                    else if (!fileDownloaded)
                        log.Error("Downloaded file " + filename + " corrupted");
                    else
                        log.Info("Downloaded file " + filename + " succesfully");
                }
                catch( Exception ex )
                {
                    fileDownloaded = false;
                    log.Error("Error during download of file", ex);
                }
                finally
                {
                    if( readStream != null )
                        readStream.Close();
                    if( saveFile != null )
                        saveFile.Close();
                }

                if (fileDownloaded)
                    break;                                
            }
            return fileDownloaded;
        }

        /// <summary>
        /// Checks whether file already is downloaded.
        /// </summary>
        /// <param name="filename"></param>
        /// <returns>Name of file to check</returns>
        private bool FileExists(string filename)
        {
            return File.Exists( Path.Combine( m_downloadPath, filename ));
        }
                        
        /// <summary>
        /// Returns hashcode of a file.
        /// </summary>
        /// <param name="path">Full name of file.</param>
        /// <returns>Hashcode</returns>
        private string GetFileHashCode(string path)
        {
            string hashCode = "";           

            using (HashAlgorithm hashAlgorithm = new SHA512Managed())
            {
                using (FileStream file = File.Open(path, FileMode.Open, FileAccess.Read))
                {
                    hashCode = BitConverter.ToString(hashAlgorithm.ComputeHash(file));
                }
            }

            return hashCode;
        }

        /// <summary>
        /// Deletes files with prefix "TEMP_" in the directory downloadPath.
        /// </summary>
        private void DeleteTemporaryFiles()
        {
            foreach (string filename in Directory.GetFiles(m_downloadPath, "TEMP_*.*", SearchOption.TopDirectoryOnly))
            {
                File.Delete(Path.Combine(m_downloadPath, filename));
            }
        }

        /// <summary>
        /// Add the latest downloaded version to the parameter.
        /// </summary>
        /// <param name="appNameWithExtension">Application name + extension</param>
        /// <returns>appNameWithExtension + version</returns>
        private string CreateAssemblyVersionString(string appNameWithExtension)
        {
            Version version = new Version(0, 0, 0, 0);
            Version newVersion = null;
            Match matchVersion = null;
            if (Directory.Exists(DownloadPath))
            {
                foreach (string filename in Directory.GetFiles(DownloadPath))
                {
                    matchVersion = Regex.Match(filename, @"^.*\\(.*?)\.");
                    if (matchVersion.Success && matchVersion.Groups[1].Value == appNameWithExtension)
                    {
                        newVersion = VersionOfFile(filename);
                        if (newVersion > version)
                        {
                            version = newVersion;
                        }
                    }
                }
            }

            return appNameWithExtension + "." + version.ToString();
        }

        /// <summary>
        /// Returns list of names of installation files with newer version than parameter.
        /// </summary>
        /// <param name="appNameWithExtension">Applicationname + extension</param>
        /// <param name="version">Installed version</param>
        /// <returns>".msi" files</returns>
        private List<String> GetFilesToInstall(string appNameWithExtension, Version version)
        {
            List<String> filesToInstall = new List<string>();
            List<Version> versions = new List<Version>();
            Version newVersion = null;
            Match matchVersion = null;
            if (Directory.Exists(DownloadPath))
            {
                foreach (string filename in Directory.GetFiles(DownloadPath))
                {
                    matchVersion = Regex.Match(filename, @"^.*\\(.*?)\.");
                    if (matchVersion.Success && matchVersion.Groups[1].Value == appNameWithExtension)
                    {
                        newVersion = VersionOfFile(filename);
                        if (newVersion > version)
                            versions.Add(newVersion);
                    }
                }
            }

            versions.Sort();
            foreach (Version lVersion in versions)                
                filesToInstall.Add(appNameWithExtension + "." + lVersion.ToString() + ".msi");

            return filesToInstall;
        }

        /// <summary>
        /// Extracts the version from a filename.
        /// </summary>
        /// <param name="filename">Name of file with version</param>
        /// <returns>Version of file</returns>
        private Version VersionOfFile(string filename)
        {
            Match match = Regex.Match(filename, @"\.(\d*\.\d*\.\d*\.\d*)\.");
            if (match.Success)
                return new Version(Regex.Match(filename, @"\.(\d*\.\d*\.\d*\.\d*)\.").Groups[1].Value);
            else
                return new Version(0, 0, 0, 0);
        }

        /// <summary>
        /// Write the date of the update in the site info of UpdaterSettings.xml.
        /// </summary>
        /// <param name="siteInfo">The site that requested the download.</param> 
        private void MarkDownloadInSiteInfo(SiteInfo siteInfo )
        {
            string path = Properties.Settings.Default.UpdaterSettingPath;
            FileStream configFile = new FileStream(path, FileMode.Open, FileAccess.Read);
            FileStream configFileForWrite = null;

            try
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(configFile);
                configFile.Close();

                XmlNode siteNode = doc.DocumentElement.SelectSingleNode("Sites/Site[@id='" + siteInfo.Id + "']");
                XmlAttribute lastUpdateAtt = siteNode.Attributes["lastUpdate"];
                lastUpdateAtt.Value = DateTime.Now.ToShortDateString();

                siteNode.Attributes["currentVersion"].Value = siteInfo.VersionNumber.ToString();

                configFileForWrite = new FileStream(path, FileMode.Create, FileAccess.Write); // Overwrite old file
                doc.Save(configFileForWrite);

            }
            catch (Exception ex)
            {
                log.Error("Could not write the time of the update in UpdaterSetting.xml file", ex);
            }
            finally
            {
                configFile.Close();
                if (configFileForWrite != null)
                    configFileForWrite.Close();

            }
        }        
                // Obsolete code
                //XmlNode downloadNode = doc.DocumentElement.SelectSingleNode("Sites/Site[@id='" + siteInfo.Id + "']/DownloadsReadyToInstall");

                //XmlNode newNode = downloadNode.AppendChild(doc.CreateNode(XmlNodeType.Element, "DownloadedFile", downloadNode.NamespaceURI));
                //XmlAttribute nameAtt = doc.CreateAttribute("name", newNode.NamespaceURI);
                //nameAtt.Value = filename;
                //newNode.Attributes.Append(nameAtt);

                //configFileForWrite = new FileStream(path, FileMode.Create, FileAccess.Write); // Overwrite old file
    }

}